package com.rogers.rmcdouga.skirmishgadget.client;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.google.gwt.dom.client.ImageElement;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.LoadEvent;
import com.google.gwt.event.dom.client.LoadHandler;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.gen2.logging.shared.Log;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.DockPanel;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
import com.google.gwt.user.client.ui.HasVerticalAlignment;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.InlineHTML;
import com.google.gwt.user.client.ui.PushButton;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.widgetideas.graphics.client.GWTCanvas;
import com.rogers.rmcdouga.skirmishgadget.client.icons.BattleMapIconBundle;
import com.rogers.rmcdouga.skirmishgadget.client.java.awt.Dimension;
import com.rogers.rmcdouga.skirmishgadget.client.java.awt.Point;
import com.rogers.rmcdouga.skirmishgadget.client.java.awt.geom.Point2D;

public class BattleMap extends Composite {
	// public members
	static final Point ORIGIN = new Point(0,0);
	static final int squareSizeInFeet = 5;
	static final float[] zoomLevelPresets = {0.06f, 0.125f, 0.25f, 0.50f, 0.75f, 1f, 1.5f, 2f, 3f, 4f, 5f, 6f};
	static final String invisibleImageContainerName = "invisibleImageContainer";
	
	// private members
	private Image map;
	private boolean initialized = false;
	
	private BattleMapCanvas canvas = new BattleMapCanvas();
	private List<Token> tokenList;
	private float zoomLevel;		// 1 = 100%, .5 = 50%, 2 = 200%, etc.
	private Dimension squareSize;
	private float panX;				// value must be >=0 and <= 1
	private float panY;				// value must be >=0 and <= 1

	private SetMapDialog dlg_SetMapDialog;
	private AddTokenDialog dlg_AddTokenDialog;
	private GridDialog dlg_GridDialog;
	private AboutDialog dlg_AboutDialog;


	public GWTCanvas getHtmlCanvas() {
		return canvas;
	}
	private void setHtmlCanvas(BattleMapCanvas canvas) {
		this.canvas = canvas;
	}
	public List<Token> getTokenList() {
		return tokenList;
	}
	protected void setTokenList(List<Token> tokenList) {
		this.tokenList = tokenList;
	}
	public Dimension getsquareSize(Point p)
	{
		// For now, this returns the same value everywhere however later it may return slightly different
		// values depending on where in the map the Point p is.
		return squareSize;
	}
	public void setFiveFootSquare(Dimension squareSize)
	{
		this.squareSize = squareSize;
	}

	public float getZoomLevel()
	{
		return zoomLevel;
	}
	public void setZoomLevel(float zoomLevel)
	{
		if (zoomLevel < 0)
			throw new IllegalArgumentException("Illegal Zoom Level value: '" + zoomLevel + "'. Zoom level cannot be < 0.");
		this.zoomLevel = zoomLevel;
	}
	public enum ZoomToFit { HEIGHT, WIDTH, BOTH };
	public void zoomToFit(ZoomToFit ztf)
	{
		float zoom_width = Integer.valueOf(canvas.getCoordWidth()).floatValue() / Integer.valueOf(map.getWidth()).floatValue();
		float zoom_height = Integer.valueOf(canvas.getCoordHeight()).floatValue() / Integer.valueOf(map.getHeight()).floatValue();
		Log.info("Inside BattleMap setZoomLevelToFitImage: myCanvas.getCoordWidth()=" + canvas.getCoordWidth() + ", myCanvas.getCoordHeight()=" + canvas.getCoordHeight() + ".");
		Log.info("Inside BattleMap setZoomLevelToFitImage: map.getWidth()=" + map.getWidth() + ", map.getHeight()=" + map.getHeight() + ".");
		Log.info("Inside BattleMap setZoomLevelToFitImage: zoom_width=" + zoom_width + ", zoom_height=" + zoom_height + ".");
		float zoom_amt;
		switch(ztf)
		{
			case HEIGHT:
				setPanY(0);
				zoom_amt = zoom_height;
				break;
			case WIDTH:
				setPanX(0);
				zoom_amt = zoom_width;
				break;
			case BOTH:
				setPanX(0);
				setPanY(0);
				zoom_amt = Math.min(zoom_width, zoom_height);
				break;
			default:
				throw new IllegalArgumentException("Unknown ZoomToFit enum value");
		}
		setZoomLevel(zoom_amt);
	}
	public void zoomInToNextPreset()
	{
		int location = Arrays.binarySearch(zoomLevelPresets, getZoomLevel());

		// If binary search fails to find the key, it returns (-(insertion point) -1).
		if (location < 0)
			location = Math.abs(location)-2;		// if the key was not found, select the location just below the insertion point

		// Now that we've got a location in mind, set it, but make sure we don't exceed the length of the array
		setZoomLevel(zoomLevelPresets[Math.min(location + 1, zoomLevelPresets.length - 1 )]);
	}
	public void zoomOutToNextPreset()
	{
		int location = Arrays.binarySearch(zoomLevelPresets, getZoomLevel());

		// If binary search fails to find the key, it returns (-(insertion point) -1).
		if (location < 0)
			location = Math.abs(location)-1;		// if the key was not found, select the location just above the insertion point

		// Now that we've got a location in mind, set it, but make sure we don't exceed the length of the array
		setZoomLevel(zoomLevelPresets[Math.max(location - 1, 0)]);
	}

	public float getPanX()
	{
		return panX;
	}
	public void setPanX(float panX)
	{
		Log.info("Inside setPanX: panX='" + panX + "'.");
		if (panX < 0 || panX > 1)
			throw new IllegalArgumentException("Illegal PanX value: '" + panX + "'. PanX must be 0-1 inclusive.");
		this.panX = panX;
	}
	public float getPanY()
	{
		return panY;
	}
	public void setPanY(float panY)
	{
		Log.info("Inside setPanY: panY='" + panY + "'.");
		if (panY < 0 || panY > 1)
			throw new IllegalArgumentException("Illegal PanY value: '" + panY + "'. PanY must be 0-1 inclusive.");
		this.panY = panY;
	}

	public enum PanDirection { LEFT, RIGHT, UP, DOWN };
	public void pan(PanDirection direction)
	{
		float map_zoomed_width = map.getWidth() * zoomLevel;
		float map_zoomed_height = map.getHeight() * zoomLevel;

		// calc pan value to scroll half a screen's width
		float pan_amt_x = (getHtmlCanvas().getCoordWidth()/2)/map_zoomed_width;
		float pan_amt_y = (getHtmlCanvas().getCoordHeight()/2)/map_zoomed_height;

		
//		Log.info("Inside pan: pan_amt='" + pan_amt + "'.");
		
		switch(direction)
		{
			case LEFT:
				float min_ratio_x = 0;
				setPanX(Math.max(min_ratio_x, getPanX()-pan_amt_x));
				break;
			case RIGHT:
				float max_ratio_x = Math.max(0, (map_zoomed_width - getHtmlCanvas().getCoordWidth())/ map_zoomed_width);
//				Log.info("Inside pan: map_zoom_size='" + map_zoomed_width + /* "', left_over_bits='" + left_over_bits + */ "', left_over_ratio='"+ max_ratio_x + "'.");
				setPanX(Math.min(max_ratio_x, getPanX()+pan_amt_x));
				break;
			case UP:
				float min_ratio_y = 0;
				setPanY(Math.max(min_ratio_y, getPanY()-pan_amt_y));
				break;
			case DOWN:
				float max_ratio_y = Math.max(0, (map_zoomed_height - getHtmlCanvas().getCoordHeight())/ map_zoomed_height);
				setPanY(Math.min(max_ratio_y, getPanY()+pan_amt_y));
				break;
			default:
				throw new IllegalArgumentException("Illegal PanDirection value: '" + direction.name() + ".value='" + direction.toString() + "'.");
		}
	}
	

	public BattleMap(String mapUrl, int width, int height) {
		this(mapUrl, new Dimension(width, height));
	}
	
	public BattleMap(String mapUrl, Dimension size) {
		super();
		Log.info("Inside BattleMap constructor: mapUrl = '" + mapUrl + ", size=" + size.width + size.height + "'.");

		// create a place to attach images
		RootPanel.get().add(new InlineHTML("<span id=\"" + invisibleImageContainerName + "\" style=\"display: none\"></span>"));
		
		// get layout stuff out of the way
		DockPanel panel = new DockPanel();
		this.canvas = new BattleMapCanvas(size.width,size.height);
		this.canvas.setTitle("Canvas");
		panel.add(this.canvas, DockPanel.CENTER);
		

		HorizontalPanel mapTokenPanel = new HorizontalPanel();
		mapTokenPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_BOTTOM);
		mapTokenPanel.add(new PushButton(new Image(BattleMapIconBundle.INSTANCE.map()), new ClickHandler(){
			@Override
			public void onClick(ClickEvent event)
			{
			    if (dlg_SetMapDialog == null) 
			    	dlg_SetMapDialog = new SetMapDialog(map.getUrl());
			    dlg_SetMapDialog.setOkAction(new SetMapDialog.DialogResult() {
					
					@Override
					public void action(SetMapDialog dlg)
					{
						canvas.resize(dlg.getMapWidth(), dlg.getMapHeight());
						setUrl(dlg.getMapURL());
						redraw();
					}
				});
			    dlg_SetMapDialog.show();
			    dlg_SetMapDialog.center();
				redraw();
			}
		}));
		mapTokenPanel.add(new PushButton(new Image(BattleMapIconBundle.INSTANCE.shape_square_add()), new ClickHandler(){
			@Override
			public void onClick(ClickEvent event)
			{
			    if (dlg_AddTokenDialog == null)
			    	dlg_AddTokenDialog = new AddTokenDialog();
			    dlg_AddTokenDialog.show();
			    dlg_AddTokenDialog.center();
				redraw();
			}
		}));
		mapTokenPanel.add(new PushButton(new Image(BattleMapIconBundle.INSTANCE.shape_square_delete()), new ClickHandler(){
			@Override
			public void onClick(ClickEvent event)
			{
				// Add code to prompt for Token to delete
				redraw();
			}
		}));
		mapTokenPanel.add(new PushButton(new Image(BattleMapIconBundle.INSTANCE.table()), new ClickHandler(){
			@Override
			public void onClick(ClickEvent event)
			{
			    if (dlg_GridDialog == null)
			    	dlg_GridDialog = new GridDialog();
			    dlg_GridDialog.show();
			    dlg_GridDialog.center();
				redraw();
			}
		}));

		HorizontalPanel zoomPanel = new HorizontalPanel();
		zoomPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_BOTTOM);
		zoomPanel.add(new PushButton(new Image(BattleMapIconBundle.INSTANCE.zoom_in()), new ClickHandler(){
			@Override
			public void onClick(ClickEvent event)
			{
				zoomInToNextPreset();
				redraw();
			}
		}));
//		HorizontalPanel topPanel2 = new HorizontalPanel();
//		topPanel2.add(new PushButton("Zoom To Fit Height", new ClickHandler(){
//			@Override
//			public void onClick(ClickEvent event)
//			{
//				zoomToFit(ZoomToFit.HEIGHT);
//				redraw();
//			}
//		}));
//		topPanel2.add(new PushButton("Zoom To Fit Width", new ClickHandler(){
//			@Override
//			public void onClick(ClickEvent event)
//			{
//				zoomToFit(ZoomToFit.WIDTH);
//				redraw();
//			}
//		}));
		DockPanel panningPanel = new DockPanel();
		panningPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE);
		panningPanel.add(new PushButton(new Image(BattleMapIconBundle.INSTANCE.arrow_out()), new ClickHandler(){
			@Override
			public void onClick(ClickEvent event)
			{
				zoomToFit(ZoomToFit.BOTH);
				redraw();
			}
		}), DockPanel.CENTER);
		panningPanel.add(new PushButton(new Image(BattleMapIconBundle.INSTANCE.arrow_left()), new ClickHandler(){
			@Override
			public void onClick(ClickEvent event)
			{
				pan(PanDirection.LEFT);
				redraw();
			}
		}), DockPanel.WEST);
		panningPanel.add(new PushButton(new Image(BattleMapIconBundle.INSTANCE.arrow_right()), new ClickHandler(){
			@Override
			public void onClick(ClickEvent event)
			{
				pan(PanDirection.RIGHT);
				redraw();
			}
		}), DockPanel.EAST);
		panningPanel.add(new PushButton(new Image(BattleMapIconBundle.INSTANCE.arrow_up()), new ClickHandler(){
			@Override
			public void onClick(ClickEvent event)
			{
				pan(PanDirection.UP);
				redraw();
			}
		}), DockPanel.NORTH);
		panningPanel.add(new PushButton(new Image(BattleMapIconBundle.INSTANCE.arrow_down()), new ClickHandler(){
			@Override
			public void onClick(ClickEvent event)
			{
				pan(PanDirection.DOWN);
				redraw();
			}
		}), DockPanel.SOUTH);
		zoomPanel.add(panningPanel);
		zoomPanel.add(new PushButton(new Image(BattleMapIconBundle.INSTANCE.zoom_out()), new ClickHandler(){
			@Override
			public void onClick(ClickEvent event)
			{
				zoomOutToNextPreset();
				redraw();
			}
		}));

		HorizontalPanel aboutPanel = new HorizontalPanel();
		aboutPanel.add(new PushButton(new Image(BattleMapIconBundle.INSTANCE.information()), new ClickHandler(){
			@Override
			public void onClick(ClickEvent event)
			{
			    if (dlg_AboutDialog == null)
			    	dlg_AboutDialog = new AboutDialog();
			    dlg_AboutDialog.show();
			    dlg_AboutDialog.center();
			}
		}));

		HorizontalPanel iconRow = new HorizontalPanel();
		iconRow.setVerticalAlignment(HasVerticalAlignment.ALIGN_BOTTOM);
		iconRow.add(mapTokenPanel);
		iconRow.add(zoomPanel);
		iconRow.add(aboutPanel);
		iconRow.setCellHorizontalAlignment(panningPanel, HasHorizontalAlignment.ALIGN_RIGHT);
		iconRow.setWidth(String.valueOf(size.width) + "px");
		panel.add(iconRow,DockPanel.NORTH);
		
		// Mandatory initWidget call (see Composite docs)
		initWidget(panel);
		
		// Give the composite a style name (I don't know why yet, but it's in the examples)
		setStyleName("BattleMap");
		
		this.tokenList = new ArrayList<Token>();
		this.map = new Image();

		// Load and Display the image
		final GWTCanvas myCanvas = canvas;	// anonymous classes prefer final variables
		this.map.addLoadHandler(new LoadHandler()
			{
				public void onLoad(LoadEvent event) {
					Log.info("Inside BattleMap Image Load event handler");
					
					panX = panY = 0;
					// Calculate a zoom level that will fit the page
					zoomToFit(ZoomToFit.BOTH);
					initialized = true;
					draw();
				}

			}
			);

		this.canvas.addMouseDownHandler(new MouseDownHandler()
		{
			public void onMouseDown(MouseDownEvent event) {
				Log.info("Inside BattleMap Mouse Down event handler - getX()=" + event.getX() + ", getY()=" + event.getY()
						+ ", getClientX()=" + event.getClientX() + ", getClientY()=" + event.getClientY()
						+ ".");
				Point mapPoint = convertScreenPointToMapPoint(new Point(event.getX(), event.getY()));
				Log.info("Checking if screenPoint " + mapPoint.x + "," + mapPoint.y + " is within Token's borders.");
				
				// find the *last* token in the list that is under the cursor (i.e. the token on top).
				Token selectedToken = null;
				for(Token t: tokenList)
				{
					if (t.isPointContained(mapPoint))
					{
						Log.info("Found a match!!!");
						selectedToken = t;
					}
				}
				if (selectedToken != null)
				{
					selectedToken.setSelected("Mover & Shaker");
					Point tokenScreenLocation = convertMapPointToScreenPoint(selectedToken.getLocation());
					int screenOffsetX = event.getX() - tokenScreenLocation.x - Math.round(getCurrentOriginX());
					int screenOffsetY = event.getY() - tokenScreenLocation.y - Math.round(getCurrentOriginY());
					
					class TokenMover
					{
						Token mySelectedToken;
						private HandlerRegistration mouseMoveHandler;
						private HandlerRegistration mouseUpHandler;
						int screenOffsetX, screenOffsetY;
						

						public TokenMover(Token mySelectedToken, int offsetX, int offsetY) {
							super();
							this.mySelectedToken = mySelectedToken;
							this.screenOffsetX = offsetX;
							this.screenOffsetY = offsetY;
						}
						
						public void createHandlers()
						{
							mouseMoveHandler = canvas.addMouseMoveHandler(new MouseMoveHandler()
							{
								public void onMouseMove(MouseMoveEvent event) {
									Log.info("Inside BattleMap Mouse Move event handler - getX()=" + event.getX() + ", getY()=" + event.getY()
											+ ", offsetX()=" + screenOffsetX + ", offsetY=" + screenOffsetY
											+ ".");
									mySelectedToken.move(convertScreenPointToMapPoint(new Point(event.getX() - screenOffsetX, event.getY() - screenOffsetY)));
								}
							}
							);
							
							mouseUpHandler = canvas.addMouseUpHandler(new MouseUpHandler()
							{
								public void onMouseUp(MouseUpEvent event) {
									Log.info("Inside BattleMap Mouse Up event handler - getX()=" + event.getX() + ", getY()=" + event.getY()
											+ ", getClientX()=" + event.getClientX() + ", getClientY()=" + event.getClientY()
											+ ".");
									mouseMoveHandler.removeHandler();
									mouseUpHandler.removeHandler();
									mySelectedToken.setDeSelected();
									redraw();
								}
							}
							);
						}
					}
					final TokenMover tokenMover = new TokenMover(selectedToken, screenOffsetX, screenOffsetY);
					tokenMover.createHandlers();
				}
			}
		}
		);
		
		setUrl(mapUrl);
	}

	public void setUrl(String mapUrl)
	{
		this.map.setUrl(mapUrl);
		// Do to a bug in GWT (http://code.google.com/p/google-web-toolkit/issues/detail?id=863), images are not loaded
		// until they are attached to the DOM, so we need to add the image into an invisible container/
		RootPanel.get(invisibleImageContainerName).add(this.map);
	}
	
	public void draw()
	{
		Log.info("Inside BattleMap draw routine: zoom=" + getZoomLevel() + ".");
		// draw the map onto the canvas
		getHtmlCanvas().saveContext();
		getHtmlCanvas().scale(getZoomLevel(), getZoomLevel());
//		getHtmlCanvas().translate(panX * map.getWidth() * getZoomLevel() / getHtmlCanvas().getCoordWidth(), panY * map.getHeight() * getZoomLevel() / getHtmlCanvas().getCoordHeight());
		getHtmlCanvas().translate(getCurrentOriginX(), getCurrentOriginY());
		getHtmlCanvas().drawImage(ImageElement.as(map.getElement()), 0, 0, map.getWidth(), map.getHeight(), 0, 0, map.getWidth(), map.getHeight());
		getHtmlCanvas().restoreContext();
	}
	public float getCurrentOriginY()
	{
		return panY * map.getHeight() * -1;
	}
	public float getCurrentOriginX()
	{
		return panX * map.getWidth() * -1;
	}
	public Point2D getCurrentOrigin2D()
	{
		return new Point2D.Float(getCurrentOriginX(), getCurrentOriginY());
	}
	public Point getCurrentOrigin()
	{
		return new Point(Math.round(getCurrentOriginX()), Math.round(getCurrentOriginY()));
	}
	public Point convertScreenPointToMapPoint(Point p)
	{
		Point2D currentOrigin = getCurrentOrigin2D();
		return new Point(Math.round((p.x / getZoomLevel()) - Double.valueOf(currentOrigin.getX()).floatValue()), Math.round((p.y / getZoomLevel()) - Double.valueOf(currentOrigin.getY()).floatValue()));
	}
	public Dimension convertScreenDimensionToMapDimension(Point p)
	{
		return new Dimension(Math.round(p.x / getZoomLevel()), Math.round(p.y / getZoomLevel()));
	}
	public Point convertMapPointToScreenPoint(Point p)
	{
		Point2D currentOrigin = getCurrentOrigin2D();
		return new Point(Math.round(Double.valueOf(currentOrigin.getX()).floatValue() + p.x * getZoomLevel()), Math.round(Double.valueOf(currentOrigin.getY()).floatValue() + p.y * getZoomLevel()));
	}
	public Dimension convertMapDimensionToScreenDimension(Point p)
	{
		return new Dimension(Math.round(p.x * getZoomLevel()), Math.round(p.y * getZoomLevel()));
	}
	

	public void redraw()
	{
		getHtmlCanvas().clear();
		draw();
		for (Token t: tokenList)
			t.draw();
	}
	
	public void refresh(Point location, Dimension size)
	{
		// TODO: This routine must be altered to do partial refresh.
		Log.info("Inside BattleMap refresh routine: zoom=" + getZoomLevel() + ".");
		// draw the map onto the canvas
		getHtmlCanvas().saveContext();
		getHtmlCanvas().scale(getZoomLevel(), getZoomLevel());
		getHtmlCanvas().drawImage(ImageElement.as(map.getElement()), 0, 0, map.getWidth(), map.getHeight(), 0, 0, map.getWidth(), map.getHeight());
		getHtmlCanvas().restoreContext();
	}
	
}
